package com.base.springboot.starter.common.http;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 封装httpclient使用时创建和回收的公共逻辑，简化 get post的代码量，用法见测试用例
 * 多例设计，每个场景仅初始化1个httplclient即可，不要频繁创建本类实例
 * TODO 1.参数基本都是写死的，根据需求重构  注意：http 响应码不是200，程序会抛异常出来，待优化
 * User: wangwei
 * Date: 2021-01-29 2:12 下午
 */
@Slf4j
public class CsHttpClient {

    final static ConcurrentHashMap<String, CsHttpClient> container = new ConcurrentHashMap<>();
    //thread safe
    final ObjectMapper writeMapper = new ObjectMapper();

    private static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    CloseableHttpClient httpClient = null;

    /**
     * 创建或获取已有的 CsHttpClient 实例，默认参数
     * @param name
     * @return
     */
    public static CsHttpClient createOrGet(String name){
        Objects.requireNonNull(name);
        container.putIfAbsent(name, new CsHttpClient(new Config()));
        return container.get(name);
    }

    /**
     * 创建或获取已有的 CsHttpClient 实例，指定参数
     * @param name
     * @param config
     * @return
     */
    public static CsHttpClient createOrGet(String name, Config config) {
        Objects.requireNonNull(name);
        Objects.requireNonNull(config,"config not allow be null,you should call createOrGet(name) in this case");
        container.putIfAbsent(name, new CsHttpClient(config));
        return container.get(name);
    }

    private CsHttpClient(Config config) {
        httpClient = HttpClients.custom()
                .setMaxConnPerRoute(config.getMaxConnPerRoute())//最大并发度m
                .setMaxConnTotal(config.getMaxConnTotal())//最多k个连接
                .setConnectionTimeToLive(-1, TimeUnit.SECONDS)
                //.evictIdleConnections(60, TimeUnit.SECONDS)//不回收连接
                .build();

        Runtime.getRuntime().addShutdownHook(
                new Thread(() -> {
                    try {
                        httpClient.close();
                    } catch (IOException ignored) {
                    }
                })
        );
    }

    /**
     * form 格式提交
     * @param url
     * @param params
     * @param callback
     * @param <R> 返回值的序列化类型
     * @return
     * @throws Exception
     */
    public <R> R postFormRequest(String url, Map<String, Object> params, CSCallback<R> callback) throws Exception {
        HttpPost httpPost = new HttpPost(encodeURL(url, DEFAULT_CHARSET.name()));
        if (params != null) {
            List<NameValuePair> pairs = params.entrySet().stream()
                    .map(e -> new BasicNameValuePair(e.getKey(), e.getValue().toString()))
                    .collect(Collectors.toList());
            httpPost.setEntity(new UrlEncodedFormEntity(pairs, DEFAULT_CHARSET.name()));
        }
        httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=" + DEFAULT_CHARSET.name());
        return request0(httpPost, callback);
    }

    /**
     * json 格式提交
     * @param url
     * @param params 使用jackson序列化对象
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R postJsonRequest(String url, Object params, CSCallback<R> callback) throws Exception {
        if (params != null) {
            return postJsonRequest(url, writeMapper.writeValueAsString(params), callback);
        }
        return postJsonRequest(url, "", callback);
    }

    /**
     * json 格式提交
     * @param url
     * @param jsonReq
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R postJsonRequest(String url, String jsonReq, CSCallback<R> callback) throws Exception {
        HttpPost httpPost = new HttpPost(encodeURL(url, DEFAULT_CHARSET.name()));
        if (!Strings.isNullOrEmpty(jsonReq)) {
            StringEntity stringEntity = new StringEntity(jsonReq, DEFAULT_CHARSET.name());
            stringEntity.setContentEncoding(DEFAULT_CHARSET.name());
            httpPost.setEntity(stringEntity);
        }
        httpPost.setHeader("Content-Type", "application/json;charset=" + DEFAULT_CHARSET.name());
        return request0(httpPost, callback);
    }

    /**
     * 
     * @param url
     * @param callback
     * @param <R>
     * @return
     * @throws Exception
     */
    public <R> R get(String url, CSCallback<R> callback) throws Exception {
        HttpGet httpGet = new HttpGet(encodeURL(url, DEFAULT_CHARSET.name()));
        return request0(httpGet, callback);
    }

    public <R> R request0(HttpRequestBase httpRequest, CSCallback<R> callback) throws Exception {

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(3000)
                .setConnectionRequestTimeout(3000)
                .setSocketTimeout(3000)
                .setStaleConnectionCheckEnabled(Boolean.TRUE)
                .build();

        httpRequest.setConfig(requestConfig);
        httpRequest.setHeader("Accept", "*/*");

        try {
            CloseableHttpResponse response = httpClient.execute(httpRequest);
            StatusLine status = response.getStatusLine();

            int state = status.getStatusCode();
            if (state == HttpStatus.SC_OK) {
                HttpEntity responseEntity = response.getEntity();
                String entityStr = EntityUtils.toString(responseEntity, DEFAULT_CHARSET);
                return callback.success(entityStr);
            } else {
                throw new RuntimeException("response status code is expected 200,actual " + response.toString());
            }
        } catch (Exception e) {
            log.error("http call fail:" + httpRequest.getURI(), e);
            throw e;
        } finally {
            httpRequest.releaseConnection();
        }
    }

    public interface CSCallback<R> {
        R success(String responseBody);
    }

    /**
     * 将URL中请求参数进行编码
     * @param url
     * @param charset
     * @return
     */
    private static String encodeURL(String url ,String charset){

        Objects.requireNonNull(url);

        if (!url.contains("?")) {
            return url;
        }

        int indexOf = url.indexOf("?");

        String uri = url.substring(0, indexOf);
        String query = url.substring(indexOf + 1);

        String[] keyAndValues = query.split("&");

        if(keyAndValues==null||keyAndValues.length==0){
            return url;
        }

        StringBuilder encodedQuery = new StringBuilder();

        int count = 0;
        for (String keyAndValue : keyAndValues) {
            count++;
            String[] arr = keyAndValue.split("=", 2);
            if (arr == null || arr.length == 0) {
                log.warn("ignore invalid param {}", keyAndValue);
                continue;
            }
            String key = arr[0];
            String value = arr[1];

            if ("null".equals(value)) {
                value = "";
            }

            if (value != null && value.length() > 0) {
                try {
                    value = URLEncoder.encode(value, charset);
                } catch (UnsupportedEncodingException e) {
                    log.warn("ignore invalid param", e);
                }
            }

            encodedQuery.append(key).append("=").append(value);

            if (count < keyAndValues.length) {
                encodedQuery.append("&");
            }
        }

        if (encodedQuery.length() > 0) {
            return encodedQuery.insert(0, "?").insert(0, uri).toString();
        }

        return uri;
    }

    public static class Config{

        private int maxConnPerRoute = 50;
        private int maxConnTotal = 50;

        public int getMaxConnPerRoute() {
            return maxConnPerRoute;
        }

        public void setMaxConnPerRoute(int maxConnPerRoute) {
            this.maxConnPerRoute = maxConnPerRoute;
        }

        public int getMaxConnTotal() {
            return maxConnTotal;
        }

        public void setMaxConnTotal(int maxConnTotal) {
            this.maxConnTotal = maxConnTotal;
        }
    }
}
